/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */
package crypto;

import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.math.BigInteger;
import java.util.logging.Level;
import java.util.logging.Logger;
import message.FileIO;
import util.Algorithms;
import util.Point;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Random;
import util.Domain;

/**
 * 
 * @author pinklight_1617181920
 */
public class Keys {

	private BigInteger privateKey;
	private Point publicKey;
	private Domain domain;
	private BigInteger hashedPassPhrase;

	public Keys(Domain domain) {

		privateKey = BigInteger.ZERO;
		publicKey = new Point();
		this.domain = domain;
		hashedPassPhrase = BigInteger.ZERO;
	}

	public Point getPublicKey() {
		return publicKey;
	}
	
	public Domain getDomain() {
		return domain;
	}

	private BigInteger getRandomBigIntInRange(Random generator) {
		BigInteger rand = BigInteger.ZERO;
		do {
			int size = domain.getPrimeOrder().byteValue();
			byte[] privateKeyBytes = new byte[size];
			generator.nextBytes(privateKeyBytes);

			// Hash Private.
			rand = new BigInteger(privateKeyBytes);
			rand = rand.mod(domain.getPrimeOrder());
		} while (rand.equals(BigInteger.ZERO));
		
		return rand;
	}
	
	private BigInteger generatePrivateKeyFromPassPhrase(String passPhrase) {
		long hashedPassPhrase = getHashAsLong(passPhrase);
		Random randomGenrator = new Random();

		// Private Key.
		randomGenrator.setSeed(hashedPassPhrase);

		return getRandomBigIntInRange(randomGenrator);
	}

	private Point generatePublicKeyFromPrivateKey(BigInteger privateKey) {
		return domain.getCurve().scalePoint(domain.getBasePoint(), privateKey);
	}

	public boolean generateKeyPairs() {
		try {

			// Get passPhrase.
			String passPhrase = FileIO.readLineFromPrompt("Please enter your passphrase:\n>");
			generateKeyPairs(passPhrase);			
			return true;

		} catch (IOException ex) {
			return false;
		}
	}
	
	public void generateKeyPairs(String passPhrase) {

		// Generate privateKey
		privateKey = generatePrivateKeyFromPassPhrase(passPhrase);

		this.hashedPassPhrase = getHash(passPhrase);

		// Public Key
		publicKey = generatePublicKeyFromPrivateKey(privateKey);		
	}

	public boolean saveKeyPair() {

		if (!hasValidKeys()) {
			return false;
		}

		BigInteger securePrivate = privateKey.xor(hashedPassPhrase);
		try {
			String username = FileIO
					.readLineFromPrompt("Please enter your username for key ownnership\n>");
			FileIO.writeMessageToFile(publicKey.toString(), "pubkey_"
					+ username + ".key");
			FileIO.writeMessageToFile(securePrivate.toString(), "seckey_"
					+ username + ".key");
			
			return true;

		} catch (IOException ex) {
			return false;
		}

	}

	/**
	 * Loads the public and private key of a user. They are used for decrypting
	 * received messages and to sign messages.
	 */
	public boolean loadKeyPair() {

		String publicKeyMessage = new String();
		String privateKeyMessage = new String();
		String username = new String();
		try {
			username = FileIO
					.readLineFromPrompt("Please enter the username of the key owner\n>");

			// Load keys from files
			publicKeyMessage = FileIO.readMessageFromFile(
					"pubkey_" + username + ".key").replaceAll("\n", "");
			privateKeyMessage = FileIO.readMessageFromFile(
					"seckey_" + username + ".key").replaceAll("\n", "");

		} catch (IOException e) {
			System.err.println(e.getLocalizedMessage());
			return false;
		}

		String passPhrase = new String();
		try {
			passPhrase = FileIO
					.readLineFromPrompt("Please enter the passphrase for \""
							+ username + "\"\n>");
		} catch (IOException e) {
			System.err.println(e.getLocalizedMessage());
			return false;
		}

		Point loadedPublicKey = new Point(publicKeyMessage);
		BigInteger loadedPrivateKeyhash = new BigInteger(privateKeyMessage);

		// Generate keys for comparison from entered passphrase
		BigInteger privateKey = generatePrivateKeyFromPassPhrase(passPhrase);

		Point publicKey = generatePublicKeyFromPrivateKey(privateKey);

		if (!publicKey.isEqual(loadedPublicKey)) {
			System.err.println("Public key mismatch. Keys were not loaded.");
			return false;
		}

		BigInteger hashedPassPhrase = getHash(passPhrase);
		BigInteger privateKeyHash = privateKey.xor(hashedPassPhrase);

		if (!privateKeyHash.equals(loadedPrivateKeyhash)) {
			System.err.println("Private key mismatch. Keys were not loaded.");
			return false;
		}

		// everything is fine. Store the values in the local memory
		this.privateKey = privateKey;
		this.publicKey = publicKey;
		this.hashedPassPhrase = hashedPassPhrase;

		return true;
	}

	/**
	 * Loads a public key of someone else to encrypt a message for that person.
	 * 
	 * @return The public key of that person.
	 */
	public static Point loadPublicKey() {

		String username = new String();
		try {
			username = FileIO.readLineFromPrompt("Please enter the username of the key owner\n>");
		} catch (IOException e) {
			System.err.println(e.getLocalizedMessage());
			return new Point();
		}
		
		return loadPublicKey(username);
	}
	
	public static Point loadPublicKey(String username) {

		String pointMessage = new String();
		try {
			pointMessage = FileIO.readMessageFromFile("pubkey_" + username + ".key").replaceAll("\n", "");
		} catch (IOException e) {
			System.err.println(e.getLocalizedMessage());
			return new Point();
		}

		return new Point(pointMessage);
		
	}

	/**
	 * 
	 * @param messagePoint
	 * @param publicKey
	 * @return
	 */
	public Point encryptPoint(Point messagePoint, Point publicKey) {
		if (!hasValidKeys()) {
			return new Point();
		}
		Point sharedKey = domain.getCurve().scalePoint(publicKey, privateKey);
		Point encryptedMessage = new Point(messagePoint.getX().add(sharedKey.getX()), messagePoint.getY().add(sharedKey.getY()));
		return encryptedMessage;
	}

	/**
	 * 
	 * @param messagePoint
	 * @param publicKey
	 * @return
	 */
	public Point decryptPoint(Point messagePoint, Point publicKey) {
		if (!hasValidKeys()) {
			return new Point();
		}
		Point sharedKey = domain.getCurve().scalePoint(publicKey, privateKey);
		Point decryptedMessage = new Point(messagePoint.getX().subtract(sharedKey.getX()), messagePoint.getY().subtract(sharedKey.getY()));
		return decryptedMessage;
	}

	public Point signPoint(Point messagePoint) {
		if (!hasValidKeys()) {
			return new Point();
		}
		
		if (messagePoint == null || messagePoint.isPointOfInfinity()) {
			return new Point();
		}
		
		BigInteger messageHash = getHash(messagePoint.toString());
		Random generator = new Random();
		while (true) {
			BigInteger v = getRandomBigIntInRange(generator);
			Point V = domain.getCurve().scalePoint(domain.getBasePoint(), v);
			if (V.isPointOfInfinity() || V.getX().equals(BigInteger.ZERO)) {
				continue;
			}
			
			BigInteger r = V.getX();
			BigInteger tmp = r.multiply(privateKey);
			tmp = tmp.add(messageHash);
			tmp = Algorithms.remainderlessDivision(tmp, v, domain.getPrimeOrder());
			BigInteger s = tmp.mod(domain.getPrimeOrder());
			if (s.equals(BigInteger.ZERO)) {
				continue;
			}

			return new Point(r, s);
		}
	}

	public boolean verifySignature(Point messagePoint, Point publicKey, Point signature) {
		if (!hasValidKeys()) {
			return false;
		}

		if (messagePoint == null || messagePoint.isPointOfInfinity()) {
			return false;
		}

		if (publicKey == null || publicKey.isPointOfInfinity()) {
			return false;
		}
		
		if (signature == null || signature.isPointOfInfinity()) {
			return false;
		}
		
		BigInteger r = signature.getX();
		BigInteger s = signature.getY();
		
		if (r.equals(BigInteger.ZERO)
				|| r.compareTo(domain.getPrimeOrder()) >= 0
				|| s.equals(BigInteger.ZERO)
				|| s.compareTo(domain.getPrimeOrder()) >= 0) {
			return false;
		}
		
		BigInteger w = Algorithms.remainderlessDivision(BigInteger.ONE, s, domain.getPrimeOrder());
		BigInteger messageHash = getHash(messagePoint.toString());
		
		BigInteger u1 = messageHash.multiply(w).mod(domain.getPrimeOrder());
		BigInteger u2 = r.multiply(w).mod(domain.getPrimeOrder());
		
		Point tmp = domain.getCurve().scalePoint(domain.getBasePoint(), u1);
		Point tmp2 = domain.getCurve().scalePoint(publicKey, u2);
		Point U = domain.getCurve().addPoints(tmp, tmp2);
		
		if (r.equals(U.getX())) {
			return true;
		}

		return false;
	}

	private static long getHashAsLong(String passPhrase) {

		BigInteger hashResult = getHash(passPhrase);
		long hashedPassPhrase = 0;
		byte[] tmp = hashResult.toByteArray();

		byte[] result = new byte[tmp.length / 2];

		for (int i = 0; i < tmp.length / 2; i++) {
			result[i] = (byte) (tmp[i] ^ tmp[i + tmp.length / 2]);

		}
		hashedPassPhrase = (new BigInteger(result)).longValue();

		return hashedPassPhrase;
	}

	private static BigInteger getHash(String message) {

		BigInteger hashedPassPhrase = BigInteger.ZERO;
		try {
			MessageDigest digest = MessageDigest.getInstance("MD5");
			digest.reset();

			byte[] input = digest.digest(message.getBytes("UTF-8"));

			hashedPassPhrase = new BigInteger(input);

		} catch (UnsupportedEncodingException ex) {
			Logger.getLogger(Keys.class.getName()).log(Level.SEVERE, null, ex);
		} catch (NoSuchAlgorithmException e) {
			Logger.getLogger(Keys.class.getName()).log(Level.SEVERE, null, e);
		}

		return hashedPassPhrase;
	}

	public boolean hasValidKeys() {
		if (publicKey == null || publicKey.isPointOfInfinity()) {
			return false;
		}
		if (privateKey == null || privateKey.equals(BigInteger.ZERO)) {
			return false;
		}

		return true;
	}
}
